//#include rfm

(function() {

var UNDEFINED,
    FRAMERATE = 33,
    _createStoryboard = (function() {
        function parseAnimValue(value) {
            var clean,
                unit;
            if (/^\#\d{6}$/.test(value)) {
                clean = [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16)];
                unit = 'rgb';
            }
            else if (/^\#\d{6}$/.test(value)) {
                clean = [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16)];
                unit = 'rgb';
            }
            else if (/^rgb/.test(value)) {
                clean = value.match(/\d*\.?\d+/g).map(parseFloat);
                unit = 'rgb';
            }
            else {
                clean = parseFloat(value);
                unit = /[^\d]+$/.exec(value);
                if (unit) {
                    unit = unit[0];
                }
            }
            return {
                unit: unit || '',
                value: clean || 0,
                toString: stringifyAnimValue
            };
        }
        function stringifyAnimValue() {
            if (this.unit == 'rgb') {
                return stringifyColor(this.value);
            }
            else {
                return this.value + this.unit;
            }
        }
        function stringifyColor(color) {
            return (color.length < 4 ? 'rgb(' : 'rgba(') + color.map(Math.round).join(',') + ')';
        }
        function tween(unit, from, to, easing, percentDone) {
            if (unit == 'rgb') {
                var result = [],
                    fn,
                    tn,
                    i;
                for (i = from.length - 1; i > -1; i--) {
                    fn = from[i];
                    tn = to[i];
                    result[i] = fn + easing(tn - fn, percentDone);
                }
                return stringifyColor(result);
            }
            else {
                return (from + easing(to - from, percentDone)) + unit;
            }
        }
        return function(target, values, duration, easing, attributeMode, arbitrary) {
            var unit,
                startMs,
                easingFunction = _easing[easing],
                properties = [],
                cleanValues = {};
            (function() {
                var computedStyle = window.getComputedStyle(target, null),
                    prop,
                    range;
                for (prop in values) {
                    range = values[prop];
                    var computedStyleUnit;
                    if (!attributeMode) {
                        computedStyleUnit = computedStyle[prop].match(/\D+/);
                        if (computedStyleUnit) {
                            computedStyleUnit = computedStyleUnit[0];
                        }
                        if (!computedStyleUnit) {
                            computedStyleUnit = '';
                        }
                    }
                    if (typeof range != 'object') {
                        range = { to: range };
                    }
                    if (range.from === UNDEFINED) {
                        range.from = computedStyle[prop];
                    }
                    else if (typeof range.from == 'number' && !attributeMode) {
                        range.from = range.from + computedStyleUnit;
                    }
                    if (typeof range.to == 'number' && !attributeMode) {
                        range.to = range.to + computedStyleUnit;
                    }
                    cleanValues[prop] = {
                        from: parseAnimValue(range.from),
                        to: parseAnimValue(range.to)
                    };
                    properties[properties.length] = prop;
                }
            })();
            return {
                target: target,
                isEmpty: function() {
                    return properties.length < 1;
                },
                has: function(node, property) {
                    return node == target && properties.indexOf(property) > -1;
                },
                values: function() {
                    return cleanValues;
                },
                remove: function(property) {
                    var i = properties.indexOf(property);
                    if (i > -1) {
                        delete cleanValues[property];
                        properties.splice(i, 1);
                    }
                },
                reset: function() {
                    startMs = UNDEFINED;
                    if (!arbitrary) {
                        var obj = attributeMode ? target : target.style;
                        for (var prop in cleanValues) {
                            obj[prop] = cleanValues[prop].from.toString();
                        }
                    }
                },
                percentDone: function(tick) {
                    return (tick - startMs) / duration;
                },
                advance: function(tick, outFrameValues) {
                    if (startMs === UNDEFINED) {
                        startMs = tick;
                    }
                    outFrameValues = outFrameValues || {};
                    var percentDone = this.percentDone(tick),
                        obj = attributeMode ? target : target.style,
                        prop;
                    if (percentDone > 1) {
                        for (prop in cleanValues) {
                            if (arbitrary) {
                                outFrameValues.step = parseFloat(cleanValues[prop].to.toString());
                                break;
                            }
                            else {
                                obj[prop] = outFrameValues[prop] = cleanValues[prop].to.toString();
                            }
                        }
                        return false;
                    }
                    else {
                        for (prop in cleanValues) {
                            var from = cleanValues[prop].from,
                                to = cleanValues[prop].to,
                                result = tween(to.unit, from.value, to.value, easingFunction, percentDone);
                            if (arbitrary) {
                                outFrameValues.step = parseFloat(result);
                                break;
                            }
                            else {
                                obj[prop] = outFrameValues[prop] = result;
                            }
                        }
                        return true;
                    }
                },
                toString: function() {
                    var propertyBuffer = [],
                        prop;
                    for (prop in cleanValues) {
                        propertyBuffer.push(prop.replace(/([A-Z])/g, '-$1').toLowerCase());
                    }
                    return [propertyBuffer.join(','), duration + 'ms', easing].join(' ');
                }
            };
        }
    })(),
    _timeline = (function() {
        var storyboards = [],
            isStopped = true,
            intervalId,
            timeoutId;
        function removeAllExistingAnimations(target, property) {
            var tempStoryboards = [].concat(storyboards),
                i,
                sb;
            for (i = tempStoryboards.length - 1; i > -1; i--) {
                sb = tempStoryboards[i];
                if ((property && sb.has(target, property)) || (!property && sb.target == target)) {
                    sb.remove(property);
                    if (sb.isEmpty()) {
                        storyboards.splice(i, 1);
                    }
                }
            }
        }
        function stop() {
            if (!isStopped) {
                isStopped = true;
                clearInterval(intervalId);
            }
        }
        function advance() {
            if (!isStopped && storyboards.length > 0) {
                var tick = new Date().getTime(),
                    tempStoryboards = [].concat(storyboards),
                    callbacks = [];
                tempStoryboards.forEach(function(sb, i) {
                    var frameValues = {};
                    if (!sb.advance(tick, frameValues)) {
                        if (sb.oncomplete) {
                            callbacks.unshift(function() {
                                sb.oncomplete.call(sb.target, frameValues);
                            });
                        }
                        storyboards.splice(i, 1);
                    }
                    else {
                        if (sb.onstep) {
                            callbacks.unshift(function() {
                                sb.onstep.call(sb.target, sb.percentDone(tick), frameValues);
                            });
                        }
                    }
                });
                if (storyboards.length < 1) {
                    stop();
                }
                for (i = 0, j = callbacks.length; i < j; i++) {
                    callbacks[i]();
                }
            }
            else {
                stop();
            }
        }
        return {
            play: function() {
                if (isStopped) {
                    clearInterval(intervalId);
                    isStopped = false;
                    if (timeoutId !== UNDEFINED) {
                        clearTimeout(timeoutId);
                    }
                    timeoutId = setTimeout(function() {
                        advance();
                        intervalId = setInterval(advance, FRAMERATE);
                        timeoutId = UNDEFINED;
                    }, 0);
                }
            },
            stop: stop,
            pop: function() {
                return storyboards.pop();
            },
            dequeue: removeAllExistingAnimations,
            queue: function(target, values, duration, easing, oncomplete, attributeMode, onstep, arbitrary) {
                var sb = _createStoryboard(target, values, duration, easing, attributeMode, arbitrary),
                    prop;
                for (prop in values) {
                    removeAllExistingAnimations(target, prop);
                }
                if (oncomplete) {
                    sb.oncomplete = oncomplete;
                }
                if (onstep) {
                    sb.onstep = onstep;
                }
                storyboards.push(sb);
                sb = null;
            }
        }
    })(),
    _easing = {
        linear: function(range, percentDone) {
            return range * percentDone;
        },
        "ease-in": function(range, percentDone) {
            return range * Math.pow(percentDone, 2);
        },
        "ease-out": function(range, percentDone) {
            return range * (1 - Math.pow(1 - percentDone, 2));
        },
        "ease-in-out": function(range, percentDone) {
            return range * (1 - Math.sin(Math.PI / 2 + percentDone * Math.PI)) / 2;
        },
        "circ-in": function(range, percentDone) {
            return -range * (Math.sqrt(1 - percentDone * percentDone) - 1);
        },
        "circ-out": function(range, percentDone) {
            return range * Math.sqrt(1 - (percentDone - 1) * (percentDone - 1));
        }
    };

/**
 * Utility for working with CSS
 */
rfm.CSS = {
    /**
     * Browser capabilities
     */
    support: {
	    /**
	     * Transition property
	     */
	    transition: false,
	    /**
	     * Transition events
	     */
	    transitionEndEvent: false,
	    /**
	     * Transform property
	     */
	    transform: false,
	    /**
	     * 3D transform functions as values to the transform property
	     */
	    transform3d: false
	},
	/**
	 * Names of different CSS properties
	 */
	properties: {
	    /**
	     * Transform property
	     */
	    transform: null,
	    /**
	     * Transition property
	     */
	    transition: null
	},
	/**
	 * Names of different CSS events
	 */
	events: {
	    /**
	     * Transition end event name
	     */
	    transitionEnd: null
	},
	/**
	 * Causes an element to be redrawn by the browser
	 * @param {Element} element The element to redraw
	 */
	repaint: function(element) {
		var className = 'rf-repaint-' + rfm.guid();
		element.className += ' ' + className;
		setTimeout(function() {
			element.className = element.className.replace(className, '');
		}, 100);
	}
};

function ontransitionend_supported(e) {
    rfm.CSS.support.transitionEndEvent = true;
    rfm.CSS.events.transitionEnd = e.type;
    this.removeEventListener('webkitTransitionEnd', arguments.callee, false);
    this.removeEventListener('transitionend', arguments.callee, false);
}

(function() {
	var div = document.createElement('div');
	div.style.cssText = '-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0)';
	if (!!div.style.webkitTransform) {
	    rfm.CSS.properties.transform = 'webkitTransform';
	}
	else if (!!div.style.MozTransform) {
	    rfm.CSS.properties.transform = 'MozTransform';
	}
	else if (!!div.style.transform) {
	    rfm.CSS.properties.transform = 'transform';
	}
	rfm.CSS.support.transform3d = rfm.CSS.support.transform = null != rfm.CSS.properties.transform;
	if (!rfm.CSS.transform) {
	    div.style.cssText = '-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);transform:rotate(45deg)';
	    if (!!div.style.webkitTransform) {
	        rfm.CSS.properties.transform = 'webkitTransform';
	    }
	    else if (!!div.style.MozTransform) {
	        rfm.CSS.properties.transform = 'MozTransform';
	    }
	    else if (!!div.style.transform) {
	        rfm.CSS.properties.transform = 'transform';
	    }
	    rfm.CSS.support.transform = null != rfm.CSS.properties.transform;
	}
	div.style.cssText = '-webkit-transition:top 1ms linear;-moz-transition:top 1ms linear;transition:top 1ms linear';
	if (div.style.webkitTransitionProperty !== UNDEFINED) {
	    rfm.CSS.properties.transition = 'webkitTransition';
	}
	else if (div.style.MozTransitionProperty !== UNDEFINED) {
	    rfm.CSS.properties.transition = 'MozTransition';
	}
	else if (div.style.transitionProperty !== UNDEFINED) {
	    rfm.CSS.properties.transition = 'transition';
	}
	if (rfm.CSS.support.transition = null != rfm.CSS.properties.transition) {
	    div.style.position = 'absolute';
		div.style.top = '-99999px';
		div.addEventListener('webkitTransitionEnd', ontransitionend_supported, false);
		div.addEventListener('transitionend', ontransitionend_supported, false);
		document.body.appendChild(div);
		setTimeout(function() {
			div.style.top = '-999px';
			setTimeout(function() {
				div.parentNode.removeChild(div);
				div = null;
			}, 100);
		}, 0);
	}
	else {
		div = null;
	}
})();

/** 
 * Tests a DOM element to see if it has a class name
 * @param {Element} element The DOM element to test
 * @param {String} className The class name to check for
 * @returns {Boolean} True if the element has the class name, false otherwise
 */
Element.prototype.$hasClass = function(className) {
	return new RegExp('(^|\\s+)' + className + '(\\s+|$)', 'g').test(this.className);
};

/**
 * Adds a class name to a DOM element, if it doesn't already have it
 * @param {Element} element The DOM element to which to add the class
 * @param {String} className The class name to add to the element
 */
Element.prototype.$addClass = function(className) {
	if (!this.$hasClass(className)) {
		this.className += ' ' + className;
	}
	return this;
};

/**
 * Removes a class name from a DOM element, if it has the class
 * @param {Element} element The DOM element from which to remove the class
 * @param {String} className The class name to remove from the element
 */
Element.prototype.$removeClass = function(className) {
	if (this.$hasClass(className)) {
		this.className = this.className.replace(new RegExp('(?:^|\\s+)' + className + '(?:\\s+|$)', 'g'), ' ');
	}
	return this;
};

/**
 * If the element has the class, remove it; otherwise add the class
 * @param {Element} element The DOM element on which to toggle the class
 * @param {String} className The class name to remove from the element 
 */
Element.prototype.$toggleClass = function(className) {
	if (this.$hasClass(className)) {
		this.$removeClass(className);
	}
	else {
		this.$addClass(className);
	}
	return this;
};

/**
 * Applies CSS properties to an element
 * @param {Object/String} properties The properties hash or the name of one property
 * @param {String} value (Optional) The value of the named property
 * @returns {Element} The element
 */
Element.prototype.$css = function(properties) {
	if (arguments.length == 2) {
		this.style[properties] = arguments[2];
	}
	else if (typeof properties == 'string') {
		return window.getComputedStyle(this, '').getPropertyValue(properties);
	}
	else {
		var buffer = [],
			name;
		for (name in properties) {
			if (properties[name] === null) {
				this.style[name] = null;
			}
			else {
				buffer.push(name, ':', properties[name], ';');
			}
		}
		this.style.cssText += buffer.join('');
	}
	return this;
};

/**
 * Transform the element
 * @param {Number} tx The x-axis translation, in pixels
 * @param {Number} ty The y-axis translation, in pixels
 * @param {Number} tz The z-axis translation, in pixels
 * @param {Number} r (Optional) The rotation, in degrees
 * @param {Number} s (Optional) The scale, as a percent
 * @returns {Element} The element
 */
Element.prototype.$transform = function(tx, ty, tz, r, s) {
    var buffer = [];
    if (tx === null) {
    	this.style[rfm.CSS.properties.transform] = null;
    }
    else {
        if (arguments.length < 2) {
            buffer.push('none');
        }
        else {
            if (rfm.CSS.support.transform3d) {
                buffer.push('translate3d(', tx, 'px,', ty, 'px,', tz, 'px)');
            }
            else {
                buffer.push('translate(', tx, 'px,', ty, 'px)');
            }
            if (r !== UNDEFINED) {
                buffer.push(' rotate(', r, 'deg)');
            }
            if (s !== UNDEFINED) {
                buffer.push(' scale(', s, ')');
            }
        }
        this.style[rfm.CSS.properties.transform] = buffer.join('');
    }
    return this;
};

/**
 * Gets the offset of the element from the screen
 * @returns {Object<string,number>} The element's left and top positions
 */
Element.prototype.$screenOffset = function() {
    var left = this.offsetLeft,
        top = this.offsetTop,
        parentNode = this.parentNode;
    while (parentNode && parentNode.nodeName != 'HTML') {
        if (parentNode.$css('position') != 'static') {
            left += parentNode.offsetLeft - parentNode.scrollLeft;
            top += parentNode.offsetTop - parentNode.scrollTop;
        }
        parentNode = parentNode.parentNode;
    }
    return {
        left: left,
        top: top
    };
};

/**
 * Animates numeric non-CSS properties
 * @param {Object} values Hash table of properties and values - or - for an arbitrary animation that does not affect the element, { from: X, to: Y }
 * @param {Number} duration Time in milliseconds that the animation should last
 * @param {String} easing The name of the easing function to use
 * @param {Function} oncomplete (Optional) A function to execute when the animation has ended
 * @param {Function} onstep (Optional) A function to execute on each step of the animation
 * @returns {Element} The element
 */
Element.prototype.$animate = function(values, duration, easing, oncomplete, onstep) {
    var arbitrary = typeof values.from == 'number' && typeof values.to == 'number';
    if (arbitrary) {
        values = { "@rfm-css-arbitrary": values };
    }
    _timeline.queue(this, values, duration || 0, easing || 'linear', oncomplete, true, onstep, arbitrary);
    _timeline.play();
    return this;
};

/**
 * Animates numeric and color CSS properties
 * @param {Object} values Hash table of CSS properties and values
 * @param {Number} duration Time in milliseconds that the animation should last
 * @param {String} easing The name of the easing function to use
 * @param {Function} oncomplete (Optional) A function to execute when the animation has ended
 * @param {Function} onstep (Optional) A function to execute on each step of the animation
 * @returns {Element} The element
 */
Element.prototype.$animateCss = function(values, duration, easing, oncomplete, onstep) {
    _timeline.queue(this, values, duration || 0, easing || 'linear', oncomplete, false, onstep, false);
    _timeline.play();
    return this;
};

/**
 * Stops all animations for an element
 * @returns {Element} the element
 */
Element.prototype.$stopAnimations = function() {
	_timeline.dequeue(this);
	return this;
};

})();